#!/usr/bin/env python3
"""
YouTube 1080p Professional Downloader
====================================

一个可靠的YouTube视频下载器，支持1080p高清视频下载
无需cookie文件，具备智能质量选择和错误处理机制

作者: AI Assistant
版本: 1.0.0
日期: 2025-07-04
"""

import os
import sys
import time
import json
import random
import logging
import argparse
from typing import Dict, List, Optional, Tuple
from pathlib import Path
from urllib.parse import urlparse, parse_qs
import re

try:
    import yt_dlp
    from yt_dlp.utils import DownloadError, ExtractorError
except ImportError:
    print("❌ 错误: 需要安装 yt-dlp")
    print("请运行: pip install yt-dlp")
    sys.exit(1)


class YouTubeDownloaderConfig:
    """下载器配置类"""
    
    # 用户代理轮换池
    USER_AGENTS = [
        'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
        'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
        'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
        'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:121.0) Gecko/20100101 Firefox/121.0',
        'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:121.0) Gecko/20100101 Firefox/121.0',
    ]
    
    # 质量降级策略
    QUALITY_FALLBACK = [
        'best[height<=1080][ext=mp4]/best[height<=1080]',
        'best[height<=720][ext=mp4]/best[height<=720]',
        'best[height<=480][ext=mp4]/best[height<=480]',
        'best[ext=mp4]/best'
    ]
    
    # 重试配置
    MAX_RETRIES = 3
    RETRY_DELAY_BASE = 2  # 基础延时（秒）
    RETRY_DELAY_MAX = 10  # 最大延时（秒）
    
    # 下载配置
    DOWNLOAD_TIMEOUT = 300  # 5分钟
    FRAGMENT_RETRIES = 5
    SOCKET_TIMEOUT = 30
    
    # 输出配置
    DEFAULT_OUTPUT_DIR = "./downloads"
    OUTPUT_TEMPLATE = "%(title)s.%(ext)s"


class YouTubeDownloaderLogger:
    """专用日志记录器"""
    
    def __init__(self, log_file: str = "youtube_downloader.log"):
        self.log_file = log_file
        self.setup_logging()
    
    def setup_logging(self):
        """设置日志记录"""
        # 创建logs目录
        log_dir = Path("logs")
        log_dir.mkdir(exist_ok=True)
        
        # 配置日志格式
        log_format = '%(asctime)s - %(name)s - %(levelname)s - %(message)s'
        date_format = '%Y-%m-%d %H:%M:%S'
        
        # 文件日志
        file_handler = logging.FileHandler(
            log_dir / self.log_file, 
            encoding='utf-8'
        )
        file_handler.setLevel(logging.DEBUG)
        file_formatter = logging.Formatter(log_format, date_format)
        file_handler.setFormatter(file_formatter)
        
        # 控制台日志
        console_handler = logging.StreamHandler()
        console_handler.setLevel(logging.INFO)
        console_formatter = logging.Formatter(
            '%(levelname)s: %(message)s'
        )
        console_handler.setFormatter(console_formatter)
        
        # 配置根日志器
        self.logger = logging.getLogger('YouTubeDownloader')
        self.logger.setLevel(logging.DEBUG)
        self.logger.addHandler(file_handler)
        self.logger.addHandler(console_handler)
        
        # 配置yt-dlp日志
        yt_dlp_logger = logging.getLogger('yt_dlp')
        yt_dlp_logger.setLevel(logging.WARNING)
        yt_dlp_logger.addHandler(file_handler)
    
    def get_logger(self):
        """获取日志器实例"""
        return self.logger


class YouTubeURLValidator:
    """YouTube URL验证器"""
    
    @staticmethod
    def validate_youtube_url(url: str) -> bool:
        """验证是否为有效的YouTube URL"""
        youtube_patterns = [
            r'https?://(www\.)?(youtube\.com|youtu\.be)',
            r'https?://m\.youtube\.com',
            r'https?://gaming\.youtube\.com',
            r'https?://music\.youtube\.com'
        ]
        
        for pattern in youtube_patterns:
            if re.match(pattern, url):
                return True
        return False
    
    @staticmethod
    def extract_video_id(url: str) -> Optional[str]:
        """提取视频ID"""
        patterns = [
            r'(?:youtube\.com/watch\?v=|youtu\.be/|youtube\.com/embed/)([^&\n?#]+)',
            r'youtube\.com/v/([^&\n?#]+)',
            r'youtube\.com/watch\?.*v=([^&\n?#]+)',
            r'youtube\.com/shorts/([^&\n?#]+)',  # 添加对YouTube Shorts的支持
            r'(?:www\.)?youtube\.com/shorts/([^&\n?#]+)'  # 包含www的Shorts URL
        ]
        
        for pattern in patterns:
            match = re.search(pattern, url)
            if match:
                return match.group(1)
        return None


class YouTubeQualitySelector:
    """智能质量选择器"""
    
    def __init__(self, logger: logging.Logger):
        self.logger = logger
        self.quality_attempts = 0
    
    def get_format_selector(self, target_quality: str = "1080p") -> str:
        """获取格式选择器"""
        quality_map = {
            "1080p": 0,
            "720p": 1,
            "480p": 2,
            "best": 3
        }
        
        start_index = quality_map.get(target_quality, 0)
        return YouTubeDownloaderConfig.QUALITY_FALLBACK[start_index]
    
    def get_next_quality_option(self) -> Optional[str]:
        """获取下一个质量选项"""
        if self.quality_attempts < len(YouTubeDownloaderConfig.QUALITY_FALLBACK):
            format_selector = YouTubeDownloaderConfig.QUALITY_FALLBACK[self.quality_attempts]
            self.quality_attempts += 1
            return format_selector
        return None
    
    def reset_attempts(self):
        """重置尝试次数"""
        self.quality_attempts = 0


class YouTubeRetryHandler:
    """重试处理器"""
    
    def __init__(self, logger: logging.Logger):
        self.logger = logger
    
    def calculate_delay(self, attempt: int) -> float:
        """计算重试延时（指数退避）"""
        delay = min(
            YouTubeDownloaderConfig.RETRY_DELAY_BASE * (2 ** attempt),
            YouTubeDownloaderConfig.RETRY_DELAY_MAX
        )
        # 添加随机抖动
        jitter = random.uniform(0, 0.1) * delay
        return delay + jitter
    
    def should_retry(self, error: Exception, attempt: int) -> bool:
        """判断是否应该重试"""
        if attempt >= YouTubeDownloaderConfig.MAX_RETRIES:
            return False
        
        # 可重试的错误类型
        retryable_errors = [
            "http error 429",  # 频率限制
            "http error 503",  # 服务不可用
            "network error",   # 网络错误
            "timeout",         # 超时
            "connection",      # 连接错误
            "temporary failure" # 临时失败
        ]
        
        error_str = str(error).lower()
        return any(retryable_error in error_str for retryable_error in retryable_errors)


class YouTubeProgressHook:
    """下载进度钩子"""
    
    def __init__(self, logger: logging.Logger):
        self.logger = logger
        self.last_progress = 0
    
    def __call__(self, d: Dict):
        """进度回调函数"""
        if d['status'] == 'downloading':
            if 'total_bytes' in d and d['total_bytes']:
                percent = (d['downloaded_bytes'] / d['total_bytes']) * 100
                if percent - self.last_progress >= 5:  # 每5%显示一次
                    self.logger.info(f"下载进度: {percent:.1f}%")
                    self.last_progress = percent
            elif '_percent_str' in d:
                percent_str = d['_percent_str'].strip()
                if percent_str != 'N/A':
                    self.logger.info(f"下载进度: {percent_str}")
        
        elif d['status'] == 'finished':
            self.logger.info(f"✅ 下载完成: {d['filename']}")
            self.last_progress = 0
        
        elif d['status'] == 'error':
            self.logger.error(f"❌ 下载错误: {d.get('error', 'Unknown error')}")


class YouTube1080pDownloader:
    """YouTube 1080p 专业下载器"""
    
    def __init__(self, output_dir: str = YouTubeDownloaderConfig.DEFAULT_OUTPUT_DIR, proxy_url: Optional[str] = None, cookies_file: Optional[str] = None):
        self.output_dir = Path(output_dir)
        self.output_dir.mkdir(exist_ok=True)
        
        # 初始化组件
        self.logger_manager = YouTubeDownloaderLogger()
        self.logger = self.logger_manager.get_logger()
        self.validator = YouTubeURLValidator()
        self.quality_selector = YouTubeQualitySelector(self.logger)
        self.retry_handler = YouTubeRetryHandler(self.logger)
        self.progress_hook = YouTubeProgressHook(self.logger)
        
        # 代理配置 - 优先使用传入的代理URL
        self.proxy_config = proxy_url if proxy_url else self._get_proxy_config()
        
        # Cookie配置
        self.cookies_file = cookies_file if cookies_file else self._get_cookies_file()
        
        self.logger.info("🚀 YouTube 1080p 下载器已初始化")
    
    def _get_proxy_config(self) -> Optional[str]:
        """获取代理配置"""
        # 常见的本地代理端口
        common_proxy_ports = ['7890', '1087', '8080', '8888', '7891', '1080']
        
        # 首先检查环境变量
        proxy_env_vars = ['HTTP_PROXY', 'http_proxy', 'HTTPS_PROXY', 'https_proxy', 'ALL_PROXY', 'all_proxy']
        for env_var in proxy_env_vars:
            proxy = os.environ.get(env_var)
            if proxy:
                self.logger.info(f"使用环境变量代理: {proxy}")
                return proxy
        
        # 检查常见的本地代理端口
        import socket
        for port in common_proxy_ports:
            proxy_url = f"http://127.0.0.1:{port}"
            try:
                sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
                sock.settimeout(1)
                result = sock.connect_ex(('127.0.0.1', int(port)))
                sock.close()
                
                if result == 0:
                    self.logger.info(f"检测到本地代理: {proxy_url}")
                    return proxy_url
            except Exception:
                continue
        
        self.logger.info("未检测到代理配置")
        return None
    
    def _get_cookies_file(self) -> Optional[str]:
        """获取cookies文件"""
        # 常见的cookies文件位置
        possible_cookies = [
            "youtube_cookies.txt",
            "./youtube_cookies.txt", 
            "./cookies/youtube_cookies.txt",
            "cookies.txt"
        ]
        
        for cookies_path in possible_cookies:
            if os.path.exists(cookies_path):
                self.logger.info(f"🍪 找到cookies文件: {cookies_path}")
                return cookies_path
        
        self.logger.warning("🍪 未找到cookies文件，可能无法下载某些受限制的视频")
        return None
    
    def set_cookies_file(self, cookies_file: Optional[str]):
        """设置cookies文件"""
        if cookies_file and os.path.exists(cookies_file):
            self.cookies_file = cookies_file
            self.logger.info(f"🍪 cookies文件已设置: {cookies_file}")
        else:
            self.cookies_file = self._get_cookies_file()
            if self.cookies_file:
                self.logger.info(f"🍪 使用自动检测的cookies文件: {self.cookies_file}")
            else:
                self.logger.warning("🍪 未找到有效的cookies文件")
    
    def set_proxy(self, proxy_url: Optional[str]):
        """设置代理配置"""
        if proxy_url:
            self.proxy_config = proxy_url
            self.logger.info(f"🌐 代理已设置: {proxy_url}")
        else:
            self.proxy_config = self._get_proxy_config()
            if self.proxy_config:
                self.logger.info(f"🌐 使用自动检测的代理: {self.proxy_config}")
            else:
                self.logger.info("🌐 未使用代理")
    
    def get_random_user_agent(self) -> str:
        """获取随机用户代理"""
        return random.choice(YouTubeDownloaderConfig.USER_AGENTS)
    
    def create_yt_dlp_options(self, target_quality: str = "1080p", strategy: str = "default") -> Dict:
        """创建yt-dlp选项"""
        options = {
            'format': self.quality_selector.get_format_selector(target_quality),
            'outtmpl': str(self.output_dir / YouTubeDownloaderConfig.OUTPUT_TEMPLATE),
            'writeinfojson': False,
            'writesubtitles': False,
            'writeautomaticsub': False,
            'extractaudio': False,
            'audioformat': 'mp3',
            'ignoreerrors': False,
            'no_warnings': False,
            'retries': YouTubeDownloaderConfig.FRAGMENT_RETRIES,
            'socket_timeout': YouTubeDownloaderConfig.SOCKET_TIMEOUT,
            'http_headers': {
                'User-Agent': self.get_random_user_agent()
            },
            'progress_hooks': [self.progress_hook],
            'prefer_ffmpeg': True,
            'merge_output_format': 'mp4',
            'postprocessors': [{
                'key': 'FFmpegVideoConvertor',
                'preferedformat': 'mp4',
            }],
        }
        
        # 根据策略调整配置
        if strategy == "mobile":
            # 模拟移动端
            options['http_headers'].update({
                'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 15_0 like Mac OS X) AppleWebKit/605.1.15',
                'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
                'Accept-Language': 'en-US,en;q=0.5',
                'Accept-Encoding': 'gzip, deflate',
                'Connection': 'keep-alive',
                'DNT': '1'
            })
        elif strategy == "tv":
            # 模拟TV客户端
            options.update({
                'extractor_args': {
                    'youtube': {
                        'player_client': ['tv']
                    }
                }
            })
        elif strategy == "ios":
            # 模拟iOS客户端
            options.update({
                'extractor_args': {
                    'youtube': {
                        'player_client': ['ios']
                    }
                }
            })
        elif strategy == "android":
            # 模拟Android客户端
            options.update({
                'extractor_args': {
                    'youtube': {
                        'player_client': ['android']
                    }
                }
            })
        
        # 添加代理配置
        if self.proxy_config:
            options['proxy'] = self.proxy_config
            self.logger.info(f"🌐 使用代理: {self.proxy_config}")
        
        # 添加cookies配置
        if self.cookies_file:
            options['cookiefile'] = self.cookies_file
            self.logger.info(f"🍪 使用cookies文件: {self.cookies_file}")
            
        return options
    
    def extract_video_info(self, url: str) -> Optional[Dict]:
        """提取视频信息"""
        try:
            info_options = {'quiet': True}
            # 添加代理配置
            if self.proxy_config:
                info_options['proxy'] = self.proxy_config
            
            # 添加cookies配置
            if self.cookies_file:
                info_options['cookiefile'] = self.cookies_file
                
            with yt_dlp.YoutubeDL(info_options) as ydl:
                info = ydl.extract_info(url, download=False)
                return info
        except Exception as e:
            self.logger.error(f"提取视频信息失败: {e}")
            return None
    
    def download_with_strategy_fallback(self, url: str, target_quality: str = "1080p") -> bool:
        """使用多种策略下载（无需cookies）"""
        strategies = ["default", "ios", "android", "tv", "mobile"]
        
        for strategy in strategies:
            self.logger.info(f"🎯 尝试策略: {strategy}")
            
            try:
                options = self.create_yt_dlp_options(target_quality, strategy)
                
                with yt_dlp.YoutubeDL(options) as ydl:
                    ydl.download([url])
                
                self.logger.info(f"✅ 策略 {strategy} 下载成功!")
                return True
                
            except Exception as e:
                self.logger.warning(f"策略 {strategy} 失败: {e}")
                continue
        
        self.logger.error("所有策略都失败了")
        return False
    
    def download_with_quality_fallback(self, url: str, target_quality: str = "1080p") -> bool:
        """使用质量降级策略下载"""
        self.quality_selector.reset_attempts()
        
        while True:
            format_selector = self.quality_selector.get_next_quality_option()
            if not format_selector:
                self.logger.error("所有质量选项都已尝试，下载失败")
                return False
            
            try:
                options = self.create_yt_dlp_options(target_quality)
                options['format'] = format_selector
                
                quality_name = self.get_quality_name_from_selector(format_selector)
                self.logger.info(f"尝试下载质量: {quality_name}")
                
                with yt_dlp.YoutubeDL(options) as ydl:
                    ydl.download([url])
                
                self.logger.info(f"✅ 成功下载 - 质量: {quality_name}")
                return True
                
            except Exception as e:
                self.logger.warning(f"质量 {quality_name} 下载失败: {e}")
                continue
    
    def get_quality_name_from_selector(self, selector: str) -> str:
        """从格式选择器获取质量名称"""
        if '1080' in selector:
            return "1080p"
        elif '720' in selector:
            return "720p"
        elif '480' in selector:
            return "480p"
        else:
            return "best available"
    
    def download_with_retry(self, url: str, target_quality: str = "1080p") -> bool:
        """带重试机制的下载（优先尝试无cookies策略）"""
        
        # 首先尝试无cookies的多策略下载
        if not self.cookies_file:
            self.logger.info("🚀 尝试无cookies多策略下载...")
            if self.download_with_strategy_fallback(url, target_quality):
                return True
        
        # 如果有cookies文件或无cookies策略失败，使用传统方法
        for attempt in range(YouTubeDownloaderConfig.MAX_RETRIES + 1):
            try:
                if attempt > 0:
                    delay = self.retry_handler.calculate_delay(attempt - 1)
                    self.logger.info(f"第 {attempt} 次重试，等待 {delay:.1f} 秒...")
                    time.sleep(delay)
                
                return self.download_with_quality_fallback(url, target_quality)
                
            except Exception as e:
                self.logger.error(f"下载尝试 {attempt + 1} 失败: {e}")
                
                if not self.retry_handler.should_retry(e, attempt):
                    self.logger.error("达到最大重试次数或遇到不可重试的错误")
                    return False
                
                continue
        
        return False
    
    def download(self, url: str, target_quality: str = "1080p") -> bool:
        """主下载方法"""
        self.logger.info(f"🎯 开始下载: {url}")
        
        # 验证URL
        if not self.validator.validate_youtube_url(url):
            self.logger.error("❌ 无效的YouTube URL")
            return False
        
        # 提取视频ID
        video_id = self.validator.extract_video_id(url)
        if not video_id:
            self.logger.error("❌ 无法提取视频ID")
            return False
        
        self.logger.info(f"📹 视频ID: {video_id}")
        
        # 提取视频信息
        video_info = self.extract_video_info(url)
        if video_info:
            title = video_info.get('title', 'Unknown')
            duration = video_info.get('duration', 0)
            uploader = video_info.get('uploader', 'Unknown')
            
            self.logger.info(f"📺 标题: {title}")
            self.logger.info(f"⏱️ 时长: {duration//60}:{duration%60:02d}")
            self.logger.info(f"👤 作者: {uploader}")
        
        # 开始下载
        success = self.download_with_retry(url, target_quality)
        
        if success:
            self.logger.info("🎉 下载完成！")
        else:
            self.logger.error("💥 下载失败")
        
        return success


def create_argument_parser() -> argparse.ArgumentParser:
    """创建命令行参数解析器"""
    parser = argparse.ArgumentParser(
        description="YouTube 1080p 专业下载器",
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog="""
使用示例:
  %(prog)s "https://www.youtube.com/watch?v=dQw4w9WgXcQ"
  %(prog)s "https://youtu.be/dQw4w9WgXcQ" --quality 720p
  %(prog)s "https://www.youtube.com/watch?v=dQw4w9WgXcQ" --output ./videos/
  %(prog)s --batch urls.txt
        """
    )
    
    parser.add_argument(
        'url',
        nargs='?',
        help='YouTube视频URL'
    )
    
    parser.add_argument(
        '--quality', '-q',
        choices=['1080p', '720p', '480p', 'best'],
        default='1080p',
        help='目标视频质量 (默认: 1080p)'
    )
    
    parser.add_argument(
        '--output', '-o',
        default=YouTubeDownloaderConfig.DEFAULT_OUTPUT_DIR,
        help='输出目录 (默认: ./downloads)'
    )
    
    parser.add_argument(
        '--batch', '-b',
        help='批量下载文件 (每行一个URL)'
    )
    
    parser.add_argument(
        '--verbose', '-v',
        action='store_true',
        help='显示详细信息'
    )
    
    parser.add_argument(
        '--version',
        action='version',
        version='YouTube 1080p Downloader v1.0.0'
    )
    
    return parser


def main():
    """主函数"""
    parser = create_argument_parser()
    args = parser.parse_args()
    
    # 创建下载器实例
    downloader = YouTube1080pDownloader(args.output)
    
    if args.verbose:
        downloader.logger.setLevel(logging.DEBUG)
    
    # 处理批量下载
    if args.batch:
        try:
            with open(args.batch, 'r', encoding='utf-8') as f:
                urls = [line.strip() for line in f if line.strip()]
            
            total_urls = len(urls)
            successful_downloads = 0
            
            print(f"\n📋 批量下载 {total_urls} 个视频")
            print("=" * 50)
            
            for i, url in enumerate(urls, 1):
                print(f"\n[{i}/{total_urls}] 处理: {url}")
                if downloader.download(url, args.quality):
                    successful_downloads += 1
                time.sleep(2)  # 避免频率限制
            
            print(f"\n✅ 批量下载完成: {successful_downloads}/{total_urls} 成功")
            
        except FileNotFoundError:
            print(f"❌ 错误: 找不到批量下载文件 '{args.batch}'")
            sys.exit(1)
        except Exception as e:
            print(f"❌ 批量下载错误: {e}")
            sys.exit(1)
    
    # 处理单个URL
    elif args.url:
        success = downloader.download(args.url, args.quality)
        sys.exit(0 if success else 1)
    
    # 无参数时显示帮助
    else:
        parser.print_help()
        sys.exit(1)


if __name__ == "__main__":
    main()